spb/wp-spboucher-ai
Public
1import Link from "next/link";2import { notFound } from "next/navigation";3import { adminPaper, taxonomy } from "@/lib/admin/queries";4import { requireUserPage } from "@/lib/auth/guard";5import { csrfToken } from "@/lib/auth/session";6import { deletePaperAction, redirectAction, relationAction, setStatusAction, updatePaperAction, versionAction } from "@/lib/admin/actions";7import { Csrf, Field, Notice, PageTitle } from "@/components/admin-ui";8import { PaperForm } from "../paper-form";9import { UploadForm } from "./upload-form";10export default async function EditPaper({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<Record<string, string | undefined>> }) {11 const { id } = await params; const sp = await searchParams; const u = await requireUserPage(); const [p, tax] = await Promise.all([adminPaper(id), taxonomy()]); if (!p) notFound(); const csrf = csrfToken(u);12 return (<div>13 <PageTitle title={`${p.identifier} · ${p.status}`}>{p.status === "published" && <Link href={`/papers/${p.slug}`} className="btn btn-sm no-underline" target="_blank">View public page ↗</Link>}</PageTitle><Notice sp={sp} />14 <section className="card p-4 mb-6 flex flex-wrap gap-2 items-center">15 <span className="label mr-2">Status</span>16 {(["draft", "reserved", "published", "withdrawn", "superseded"] as const).filter((st) => st !== p.status).map((st) => <form key={st} action={setStatusAction} className="inline-flex gap-1 items-center"><Csrf token={csrf} /><input type="hidden" name="id" value={p.id} /><input type="hidden" name="status" value={st} />{(st === "withdrawn" || st === "superseded") && <input name="reason" placeholder="reason (public)" className="input !w-44 !py-1 !text-xs" />}<button className={`btn btn-sm ${st === "published" ? "btn-primary" : ""}`} {...(st === "published" && !p.versions.some((v: Record<string, any>) => v.file_id) ? { title: "Upload a manuscript first (allowed without, but no PDF will be offered)" } : {})}>{st === "published" ? "Publish" : `Mark ${st}`}</button></form>)}17 {p.status !== "published" && <form action={deletePaperAction} className="ml-auto"><Csrf token={csrf} /><input type="hidden" name="id" value={p.id} /><button className="btn btn-sm text-bad">Delete draft</button></form>}18 <p className="w-full text-xs text-fg-3 mt-1">Publishing requires explicit approval here; nothing is published automatically. Withdrawn papers keep their record and identifier.</p>19 </section>20 <PaperForm p={p} tax={tax} csrf={csrf} action={updatePaperAction} />21 <section id="versions" className="card p-4 mt-8"><h2 className="text-lg mb-3">Versions</h2>22 <table className="data"><thead><tr><th>#</th><th>Label</th><th>Date</th><th>Description</th><th>File</th><th>Actions</th></tr></thead><tbody>{p.versions.map((v: Record<string, any>) => <tr key={v.id}><td>{v.version_no}{v.is_current && <span className="chip chip-navy ml-1">current</span>}{v.withdrawn && <span className="chip chip-burgundy ml-1">withdrawn</span>}</td>23 <td colSpan={3}><form action={versionAction} className="grid sm:grid-cols-[6rem_8rem_1fr_auto] gap-2 items-end"><Csrf token={csrf} /><input type="hidden" name="version_id" value={v.id} /><input type="hidden" name="op" value="update" /><input name="label" defaultValue={v.label} className="input !py-1 !text-xs" aria-label="label" /><input name="released_on" defaultValue={v.released_on ? (v.released_precision === "month" ? String(v.released_on).slice(0, 7) : v.released_precision === "year" ? String(v.released_on).slice(0, 4) : String(v.released_on).slice(0, 10)) : ""} className="input !py-1 !text-xs" aria-label="date" /><input name="description" defaultValue={v.description ?? ""} className="input !py-1 !text-xs" aria-label="description" /><button className="btn btn-sm">Save</button></form></td>24 <td className="text-xs">{v.file_id ? <>{v.pages ?? "?"} p. · {v.integrity_ok === false ? <span className="text-bad">integrity failed</span> : v.integrity_ok ? "ok" : "pending"} · text {v.text_status}<br /><code>{String(v.sha256 ?? "").slice(0, 12)}</code> · <a href={`/api/files/${v.file_id}/${p.slug}-${v.label}.pdf`} className="text-navy">PDF</a></> : "—"}</td>25 <td className="whitespace-nowrap"><div className="flex gap-1">{!v.is_current && <form action={versionAction}><Csrf token={csrf} /><input type="hidden" name="version_id" value={v.id} /><input type="hidden" name="op" value="make_current" /><button className="btn btn-sm">Make current</button></form>}26 {v.withdrawn ? <form action={versionAction}><Csrf token={csrf} /><input type="hidden" name="version_id" value={v.id} /><input type="hidden" name="op" value="restore" /><button className="btn btn-sm">Restore</button></form> : <form action={versionAction} className="flex gap-1"><Csrf token={csrf} /><input type="hidden" name="version_id" value={v.id} /><input type="hidden" name="op" value="withdraw" /><input name="reason" placeholder="legal/privacy reason" className="input !w-36 !py-1 !text-xs" /><button className="btn btn-sm text-bad">Withdraw</button></form>}</div></td></tr>)}</tbody></table>27 <h3 className="text-base mt-6 mb-2">Upload a new version (PDF)</h3><UploadForm paperId={p.id} csrf={csrf} nextNo={(p.versions[0]?.version_no ?? 0) + 1} />28 </section>29 <section id="relations" className="card p-4 mt-8 grid md:grid-cols-2 gap-6">30 <div><h2 className="text-lg mb-3">Relations ({p.relations.length})</h2><ul className="text-sm space-y-1 max-h-72 overflow-auto">{p.relations.map((r: Record<string, any>) => <li key={r.id} className="flex gap-2 items-start"><span className="chip shrink-0">{r.kind}{r.inferred ? " · inferred" : ""}</span><span className="text-fg-2"><span className="font-mono text-navy">{r.identifier}</span> {r.title} <span className="text-fg-3">— {r.explanation}</span></span>{r.kind === "curated" && <form action={relationAction}><Csrf token={csrf} /><input type="hidden" name="paper_id" value={p.id} /><input type="hidden" name="op" value="remove" /><input type="hidden" name="relation_id" value={r.id} /><button className="text-bad text-xs">remove</button></form>}</li>)}</ul></div>31 <form action={relationAction} className="space-y-2"><h3 className="text-base">Add a curated relation (both directions)</h3><Csrf token={csrf} /><input type="hidden" name="paper_id" value={p.id} /><input type="hidden" name="op" value="add" /><Field label="Target identifier or slug" name="to_slug" required /><Field label="Explanation (EN)" name="explanation" /><Field label="Explanation (FR)" name="explanation_fr" /><button className="btn btn-sm">Add</button></form>32 </section>33 <section className="card p-4 mt-8"><h2 className="text-lg mb-3">Redirects to this paper</h2><ul className="text-sm">{p.redirects.map((r: Record<string, any>) => <li key={r.from_path} className="flex gap-2 items-center"><code>{r.from_path}</code> → <code>{r.to_path}</code> ({r.status})<form action={redirectAction}><Csrf token={csrf} /><input type="hidden" name="op" value="remove" /><input type="hidden" name="from_path" value={r.from_path} /><input type="hidden" name="back" value={`/admin/papers/${p.id}`} /><button className="text-bad text-xs ml-2">remove</button></form></li>)}</ul>34 <form action={redirectAction} className="flex flex-wrap gap-2 items-end mt-3"><Csrf token={csrf} /><input type="hidden" name="op" value="add" /><input type="hidden" name="to_path" value={`/papers/${p.slug}`} /><input type="hidden" name="back" value={`/admin/papers/${p.id}`} /><Field label="Historical path (e.g. /research/old-slug)" name="from_path" required /><Field label="Note" name="note" /><button className="btn btn-sm">Add redirect</button></form></section>35 </div>);36}37